#!/usr/bin/python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import dbus
import glob
import pyudev
import pathlib
from .util import Util
from .util_edid import EdidUtil
from .handy import MachineHwcfgFile


class SysSnapshot:

    def __init__(self):
        self._kernelObj = SysSnapshotKernel()
        self._cpuObj = SysSnapshotCpu()
        self._udevObj = SysSnapshotUdev()
        self._monitorObj = SysSnapshotMonitor()
        self._bluetoothObj = SysSnapshotBluetooth()
        self._printerObj = SysSnapshotPrinter()
        self._localNetworkObj = SysSnapshotLocalNetwork()
        self._savedCompObj = SysSnapshotSavedComponents()
        self._dumbCompObj = SysSnapshotDumbComponents()

    @property
    def kernel(self):
        return self._kernelObj

    @property
    def cpu(self):
        return self._cpuObj

    @property
    def udev(self):
        return self._udevObj

    @property
    def monitor(self):
        return self._monitorObj

    @property
    def printer(self):
        return self._printerObj

    @property
    def bluetooth(self):
        return self._bluetoothObj

    @property
    def saved(self):
        return self._savedCompObj

    @property
    def dumb(self):
        return self._dumbCompObj


class SysSnapshotKernel:

    def __init__(self):
        self._kernelConfig = None
        if True:
            pass                                                            # FIXME

        self._kernelCmdline = None
        self._kernelCmdlineParsed = None
        if True:
            self._kernelCmdline = pathlib.Path("/proc/cmdline").read_text()
            self._kernelCmdlineParsed = self._kernelCmdline.split(" ")      # FIXME

    def get_config(self):
        return self._kernelConfig

    def get_cmdline(self, parsed=False):
        if not parsed:
            return self._kernelCmdline
        else:
            return self._kernelCmdlineParsed


class SysSnapshotCpu:

    class Core:

        def __init__(self, index):
            self.index = index
            self.properties = dict()
            self.used = False

    def __init__(self):
        self._cpuCores = []
        try:
            for line in pathlib.Path("/proc/cpuinfo").read_text().split("\n"):
                m = re.fullmatch(r"(.*?)\s+:\s+(.*)", line)
                if m is not None:
                    if m.group(1) == "processor":
                        i = int(m.group(2))
                        assert i == len(self._cpuCores)
                        self._cpuCores.append(self.Core(i))
                        continue
                    else:
                        self._cpuCores[-1].properties[m.group(1)] = m.group(2)
        except FileNotFoundError:
            raise SystemError("/proc/cpuinfo does not exist")
        except ValueError:
            raise SystemError("invalid content in /proc/cpuinfo")

    def query_cores(self):
        ret = []
        for core in self._cpuCores:
            if not core.used:
                ret.append(core)
        return ret

    def query_core_by_index(self, index):
        ret = self._cpuCores[index]
        if not ret.used:
            return ret
        return None

    def use_cores(self, cores, strict=True):
        _useMultiple(cores, strict)

    def use_core(self, core):
        _useSingle(core)


class SysSnapshotUdev:

    class Device:

        def __init__(self, dev):
            self.dev = dev
            self.used = False

    def __init__(self):
        self._devs = [self.Device(x) for x in pyudev.Context().list_devices()]

    def query_devices(self, filter_functions=[]):
        return _queryAll(self._devs, filter_functions)

    def query_first_device(self, filter_functions=[]):
        return _queryFirst(self._devs, filter_functions)

    def query_first_device_in_devices(self, devices, filter_functions=[]):
        return _queryFirst(devices, filter_functions)

    def query_device_tree_by_dev(self, device, include_root=True):
        ret = []
        if include_root:
            ret.append(device)
            assert not device.used
        for d in self._devs:
            if d.dev["DEVPATH"].startswith(device.dev["DEVPATH"] + "/"):
                assert not d.used
                ret.append(d)
        return ret

    def query_device_tree_by_devpath(self, devpath, include_root=True):
        ret = []
        if include_root:
            for d in self._devs:
                if d.dev["DEVPATH"] == devpath:
                    assert not d.used
                    ret.append(d)
            assert len(ret) == 1
        for d in self._devs:
            if d.dev["DEVPATH"].startswith(devpath + "/"):
                assert not d.used
                ret.append(d)
        return ret

    def query_first_device_in_device_tree_by_dev(self, device, filter_functions=[], include_root=True):
        ret = self.query_device_tree_by_dev(device, include_root)
        return _queryFirst(ret, filter_functions)

    def query_first_device_in_device_tree_by_devpath(self, devpath, filter_functions=[], include_root=True):
        ret = self.query_device_tree_by_devpath(devpath, include_root)
        return _queryFirst(ret, filter_functions)

    def query_device_by_devpath(self, devpath):
        return _querySingle(self._devs, [lambda x: x.dev["DEVPATH"] == devpath])

    def use_devices(self, devices, strict=True):
        _useMultiple(devices, strict)

    def use_device(self, device):
        _useSingle(device)


class SysSnapshotMonitor:

    class Device:

        def __init__(self, connector, edid):
            self.connector = connector
            self.edid = edid
            self.used = False

    def __init__(self):
        # FIXME: this is bad, drmProbe() can not probe device of other seat
        # should write an udev rule to probe after /dev/dri/card device appears
        Util.drmProbe()

        # FIXME: change to python-drm?
        self._devs = []
        for fullfn in glob.glob("/sys/class/drm/card*/card*/status"):
            if pathlib.Path(fullfn).read_text().rstrip() == "connected":
                connectorDir = os.path.dirname(fullfn)
                connector = os.path.basename(connectorDir)
                edidFullfn = os.path.join(connectorDir, "edid")
                edidBuf = pathlib.Path(connector, edidFullfn).read_bytes()
                edid = EdidUtil.parse_edid(edidBuf)
                self._devs.append(self.Device(connector, edid))

    def query_devices(self, filter_functions=[]):
        return _queryAll(self._devs, filter_functions)

    def query_first_device(self, filter_functions=[]):
        return _queryFirst(self._devs, filter_functions)

    def use_devices(self, devices, strict=True):
        _useMultiple(devices, strict)

    def use_device(self, device):
        _useSingle(device)


class SysSnapshotBluetooth:

    class Device:

        def __init__(self, mac_address, connected, properties, profiles):
            self.mac_address = mac_address
            self.is_connected = connected
            self.properties = properties
            self.profiles = profiles
            self.used = False

    def __init__(self):
        self._devs = []
        try:
            bus = dbus.SystemBus()
            obj = bus.get_object('org.bluez', '/')
            if obj is not None:
                manager = dbus.Interface(obj, 'org.freedesktop.DBus.ObjectManager')
                for dev_path, dev_props in manager.GetManagedObjects().items():
                    if 'org.bluez.Device1' not in dev_props.keys():
                        continue
                    properties = dev_props['org.bluez.Device1']
                    profiles = None
                    if properties['Connected']:
                        # device = dbus.Interface(bus.get_object('org.bluez', dev_path), 'org.bluez.Device1')
                        profiles = properties['UUIDs']
                    dev = self.Device(properties['Address'], properties['Connected'], properties, profiles)
                    self._devs.append(dev)
        except dbus.exceptions.DBusException:
            # dbus does not exist, whith means no bluetooth device
            pass

    def query_devices(self, filter_functions=[]):
        return _queryAll(self._devs, filter_functions)

    def query_first_device(self, filter_functions=[]):
        return _queryFirst(self._devs, filter_functions)

    def query_device_by_mac_address(self, mac_address):
        return _querySingle(self._devs, [lambda x: x.mac_address == mac_address])

    def use_devices(self, devices, strict=True):
        _useMultiple(devices, strict)

    def use_device(self, device):
        _useSingle(device)


class SysSnapshotPrinter:

    class Device:

        def __init__(self, name, make_and_model, url):
            self.name = name
            self.make_and_model = make_and_model
            self.url = url
            self.used = False

    def __init__(self):
        self._devs = []
        try:
            import cups
            try:
                conn = cups.Connection()
                for name, info in conn.getPrinters().items():
                    dev = self.Device(name, info["printer-make-and-model"], info["device-uri"])
                    self._devs.append(dev)
            except RuntimeError:
                # cups daemon is not running, which means no printer
                pass
        except ModuleNotFoundError:
            # pycups is not instlled
            pass

    def query_devices(self, filter_functions=[]):
        return _queryAll(self._devs, filter_functions)

    def query_first_device(self, filter_functions=[]):
        return _queryFirst(self._devs, filter_functions)

    def query_device_by_mac_address(self, mac_address):
        return _querySingle(self._devs, [lambda x: x.mac_address == mac_address])

    def use_devices(self, devices, strict=True):
        _useMultiple(devices, strict)

    def use_device(self, device):
        _useSingle(device)


class SysSnapshotLocalNetwork:

    class Service:

        def __init__(self):
            self.used = False

    def query_services(self, filter_functions=[]):
        return _queryAll(self._services, filter_functions)

    def query_first_service(self, filter_functions=[]):
        return _queryFirst(self._services, filter_functions)

    def use_services(self, services, strict=True):
        _useMultiple(services, strict)

    def use_service(self, service):
        _useSingle(service)


class SysSnapshotSavedComponents:

    class Component:

        def __init__(self, data):
            self.data = data
            self.used = False

    def __init__(self):
        self._components = [self.Component(x) for x in MachineHwcfgFile.catSaved().read()]

    def query_components(self, filter_functions=[]):
        return _queryAll(self._components, filter_functions)

    def use_components(self, components, strict=True):
        _useMultiple(components, strict)

    def use_component(self, component):
        _useSingle(component)


class SysSnapshotDumbComponents:

    class Component:

        def __init__(self, data):
            self.data = data
            self.used = False

    def __init__(self):
        self._components = [self.Component(x) for x in MachineHwcfgFile.catDumb().read()]

    def query_components(self, filter_functions=[]):
        return _queryAll(self._components, filter_functions)

    def use_components(self, components, strict=True):
        _useMultiple(components, strict)

    def use_component(self, component):
        _useSingle(component)


def _filteredOut(item, filter_functions):
    for func in filter_functions:
        if not func(item):
            return True
    return False


def _queryAll(itemList, filterFunctionList):
    ret = []
    for item in itemList:
        if not item.used and not _filteredOut(item, filterFunctionList):
            ret.append(item)
    return ret


def _queryFirst(itemList, filterFunctionList):
    for item in itemList:
        if not item.used and not _filteredOut(item, filterFunctionList):
            return item
    return None


def _querySingle(itemList, filterFunctionList):
    ret = None
    for item in itemList:
        if not item.used and not _filteredOut(item, filterFunctionList):
            assert ret is None
            ret = item
    return ret


def _useMultiple(itemList, strict):
    for item in itemList:
        if strict:
            assert not item.used
        item.used = True


def _useSingle(item):
    assert not item.used
    item.used = True
